//
//  HTMLParser.swift
//  html_to_json
//
//  Created by zhu xietong on 2018/2/1.
//  Copyright © 2018年 zhu xietong. All rights reserved.
//

import Foundation
//import Regex

public enum HTMLTagType:String{
    case div = "div"
    case img = "img"
    case h1 = "h1"
    case h2 = "h2"
    case h3 = "h3"
    case h4 = "h4"
    case h5 = "h5"
    case h6 = "h6"
    case span = "span"
    case p = "p"
    case unkown = ""
   
    
}

public class EeXMLParser{
    
    public var rootElement:XMLElemet? = nil
    public var parseringElement:XMLElemet? = nil
    public var parentElements:[XMLElemet] = [XMLElemet]()
    public var htmlElements:[XMLElemet] = [XMLElemet]()
    
    public init() {
        
    }
    
    public static func scan(xmlString:String)->XMLElemet?{
        let paser = EeXMLParser()
        paser.parser(html: xmlString)
        return paser.rootElement
        
    }
    
    public static func scan(file:String)->XMLElemet?{
        let paser = EeXMLParser()
        paser.parser(file: file)
        return paser.rootElement
    }
    
    public func parser(file:String) {
        //            NSString *xml = [NSString stringWithContentsOfFile:path1 encoding:NSUTF8StringEncoding error:nil];
        //            let path = Bundle.main.path(forResource: "DeviceInfoKit", ofType: "xml", inDirectory:"html")
        do {
            let html = try String(contentsOfFile: file, encoding: String.Encoding.utf8)
            let formatHtml = html.formatHTML
            let lines = formatHtml.htmlLines
            
            for one in lines
            {
                read(line: one)
            }
        } catch {
            
        }
        
        
    }
    
    public func parser(html:String) {
        let formatHtml = html.formatHTML
        let lines = formatHtml.htmlLines
        for one in lines
        {
            read(line: one)
        }
    }
    
    
    public func read(line:String) {
        
        
        let lineType = LineType.Get(line: line)
        switch lineType{
        case .header:
            if let tagName = line.htmlTagName()
            {
                let element = XMLElemet()
                element.tagName = tagName
                element.atrribute = line.htmlAtrributes()
                if self.parentElements.count > 0
                {
                    if let parent = parentElements.last
                    {
                        element.relationShip = "\(parent.relationShip).\(element.tagName)"
                        element.parent = parent
                        parent.childs.append(element)
                    }
                }
                else{
                    element.relationShip = tagName
                    element.isRoot = true
                    self.rootElement = element
                }
                parentElements.append(element)
                self.parseringElement = element
                self.htmlElements.append(element)
            }
        case .footer:
            if parentElements.count > 0
            {
                parentElements.removeLast()
            }
        case .changeLine:
            if let parent = parentElements.last{
                let element = XMLElemet()
                element.tagName = "lineBreak"
                element.relationShip = "\(parent.relationShip).\(element.tagName)"
                element.parent = parent
                element.childs.append(element)
                htmlElements.append(element)
            }
        case .content:
            if let element = parseringElement{
                element.content = line
            }
        case .holeTag:
            let element = XMLElemet()
            let tagName = line.htmlTagName() ?? ""
            element.tagName = tagName
            element.atrribute = line.htmlAtrributes()
            if parseringElement == nil{
                rootElement = element
            }
            if parentElements.count > 0
            {
                if let parent = parentElements.last{
                    element.relationShip = "\(parent.relationShip).\(element.tagName)"
                    element.parent = parent
                    parent.childs.append(element)
                    //                            print("--dd|===\(parent.tagName)")
                    
                }
            }
            else{
                element.relationShip = tagName
                element.isRoot = true
                rootElement = element
            }
            htmlElements.append(element)
            
        default:
            break
        }
        
    }
    
}



extension EeXMLParser{
    
    public class XMLElemet {
        public var tagName = "div"
        public var content = ""
        //        var text = ""
        
        public var atrribute:[String:Any] = [String:Any]()
        public weak var parent:XMLElemet? = nil
        public var childs:[XMLElemet] = [XMLElemet]()
        
        public var relationShip = ""
        
        public var isRoot = false
        
        public var dict:[String:Any]{
            get{
                var dict = [String:Any]()
                dict["tagName"] = tagName
                if content.count > 0
                {
                    dict["content"] = content
                }
                //                if text.count > 0
                //                {
                //                    dict["text"] = text
                //                }
                if atrribute.count > 0
                {
                    dict["atrribute"] = atrribute
                }
                if childs.count > 0
                {
                    dict["childs"] = childs.map({ (element) -> [String:Any] in
                        return element.dict
                    })
                }
                return dict
            }
        }
        
        public var displayTags:[[String:Any]]{
            get{
                var list = [[String:Any]]()
                var item = [String:Any]()
                
                
                
                
                let tagType = HTMLTagType(rawValue: tagName) ?? .unkown
                switch tagType {
                case .img:
                    content = atrribute.mutable_dictionary["src",""]
                default:
                    break
                }
                
                if content.len > 0
                {
                    item["content"] = content
                    item["atrribute"] = atrribute
                    item["tagName"] = tagName
                    list.append(item)
                }
                
                
                for one in childs {
                    list = list + one.displayTags
                }
                return list
            }
            
        }
        
        public var items:[[String:Any]]{
            get{
                var list = [[String:Any]]()
                var item = [String:Any]()
                
                item["content"] = content
                item["atrribute"] = atrribute
                item["tagName"] = tagName
                
                
                list.append(item)
                for one in childs {
                    list = list + one.items
                }
                return list
            }
        }
        
    }
    
    enum LineType:String{
        case header = "header"
        case footer = "footer"
        case content = "content"
        case holeTag = "holeTag"
        case changeLine = "changeLine"
        case validateStr = "validateStr"
        
        static func Get(line:String) ->LineType
        {
            let holeTags = ["img"]
            var type:LineType = .content
            
            for one in holeTags
            {
                if line.hasPrefix("<\(one)"){
                    type = .holeTag
                }
            }
            if type != .content
            {
                return type
            }
            
            if let _ = Regex("<.*br>").firstMatch(in: line)
            {
                return LineType.changeLine
            }
            
            if let _ = Regex("<[^/]\\s*.*?[^/]>").firstMatch(in: line)
            {
                return LineType.header
            }
            if let _ = Regex("<[\\w\\d_]+>").firstMatch(in: line)
            {
                return LineType.header
            }
            if let _ = Regex("</\\s*.*?>").firstMatch(in: line)
            {
                return LineType.footer
            }
            if let _ = Regex("<.*/>").firstMatch(in: line)
            {
                return LineType.holeTag
            }
            
            if line == "    "
            {
                return LineType.validateStr
            }
            
            return type
        }
        
    }
}

extension String{
    
    
    fileprivate func htmlAtrributes() -> [String:Any] {
        var atrrs = [String:Any]()
        
        let atrrbuttes = Regex("\\S+\\s*=\\s*\"[^\"]*\"").allMatches(in: "\(self)").map { (result) -> String in
            return result.matchedString
        }
        for one in atrrbuttes {
            var atributeName = Regex("\\S+\\s*(?=\\=)").firstMatch(in: one)?.matchedString ?? ""
            var valueStr = Regex("\"[^\"]*\"").firstMatch(in: one)?.matchedString ?? ""
            atributeName.replaceAll(matching: "^\\s*", with: "")
            atributeName.replaceAll(matching: "\\s*$", with: "")
            
            valueStr.replaceAll(matching: "^\\s*", with: "")
            valueStr.replaceAll(matching: "\\s*$", with: "")
            
            
            let s = valueStr.index(valueStr.startIndex, offsetBy: 1)
            let e = valueStr.index(valueStr.endIndex, offsetBy: -1)
            valueStr = String(valueStr[s..<e])
            
            atrrs[atributeName] = valueStr
            
        }
        return atrrs
        
    }
    
    
    
    fileprivate func htmlTagName()->String? {
        if let str = Regex("<\\w+").firstMatch(in: self)?.matchedString
        {
            var valueStr = str
            let s = valueStr.index(valueStr.startIndex, offsetBy: 1)
            let e = valueStr.endIndex
            valueStr = String(valueStr[s..<e])
            return valueStr
        }
        return nil
    }
    
    fileprivate var formatHTML:String{
        get{
            var newHtml = "\(self)"
            newHtml.replaceAll(matching: "<!--.*?-->", with: "")
            newHtml.replaceAll(matching: "&nbsp;", with: " ")
            newHtml.replaceAll(matching: "(?<=[^\n])<", with: "\n<")
            newHtml.replaceAll(matching: ">(?=[^\n])", with: ">\n")
            newHtml.replaceAll(matching: "\n\\s*\n", with: "\n")
            //            print("\n\n\(newHtml)\n\n")
            
            return newHtml
        }
    }
    
    fileprivate var htmlLines:[String]{
        get{
            return Regex(".+?(?=\n)").allMatches(in: "\(self)").map { (result) -> String in
                return result.matchedString
            }
        }
    }
    
}


